// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/*
 *******************************************************************************
 * Copyright (C) 2009-2015, International Business Machines Corporation and    *
 * others. All Rights Reserved.                                                *
 *******************************************************************************
 */
package com.ibm.icu.text;

import com.ibm.icu.impl.Grego;
import com.ibm.icu.impl.Utility;
import com.ibm.icu.util.Currency.CurrencyUsage;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.Date;
import java.util.List;

/**
 * Provides information about currencies that is not specific to a locale.
 *
 * <p>A note about currency dates. The CLDR data provides data to the day, inclusive. The date
 * information used by CurrencyInfo and CurrencyFilter is represented by milliseconds, which is
 * overly precise. These times are in GMT, so queries involving dates should use GMT times, but more
 * generally you should avoid relying on time of day in queries.
 *
 * <p>This class is not intended for public subclassing.
 *
 * @stable ICU 4.4
 */
public class CurrencyMetaInfo {
    private static final CurrencyMetaInfo impl;
    private static final boolean hasData;

    /**
     * Returns the unique instance of the currency meta info.
     *
     * @return the meta info
     * @stable ICU 4.4
     */
    public static CurrencyMetaInfo getInstance() {
        return impl;
    }

    /**
     * Returns the unique instance of the currency meta info, or null if noSubstitute is true and
     * there is no data to support this API.
     *
     * @param noSubstitute true if no substitute data should be used
     * @return the meta info, or null
     * @stable ICU 49
     */
    public static CurrencyMetaInfo getInstance(boolean noSubstitute) {
        return hasData ? impl : null;
    }

    /**
     * Returns true if there is data for the currency meta info.
     *
     * @return true if there is actual data
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public static boolean hasData() {
        return hasData;
    }

    /**
     * Subclass constructor.
     *
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    protected CurrencyMetaInfo() {}

    /**
     * A filter used to select which currency info is returned.
     *
     * @stable ICU 4.4
     */
    public static final class CurrencyFilter {
        /**
         * The region to filter on. If null, accepts any region.
         *
         * @stable ICU 4.4
         */
        public final String region;

        /**
         * The currency to filter on. If null, accepts any currency.
         *
         * @stable ICU 4.4
         */
        public final String currency;

        /**
         * The from date to filter on (as milliseconds). Accepts any currency on or after this date.
         *
         * @stable ICU 4.4
         */
        public final long from;

        /**
         * The to date to filter on (as milliseconds). Accepts any currency on or before this date.
         *
         * @stable ICU 4.4
         */
        public final long to;

        /**
         * true if we are filtering only for currencies used as legal tender.
         *
         * @internal
         * @deprecated This API is ICU internal only.
         */
        @Deprecated public final boolean tenderOnly;

        private CurrencyFilter(
                String region, String currency, long from, long to, boolean tenderOnly) {
            this.region = region;
            this.currency = currency;
            this.from = from;
            this.to = to;
            this.tenderOnly = tenderOnly;
        }

        private static final CurrencyFilter ALL =
                new CurrencyFilter(null, null, Long.MIN_VALUE, Long.MAX_VALUE, false);

        /**
         * Returns a filter that accepts all currency data.
         *
         * @return a filter
         * @stable ICU 4.4
         */
        public static CurrencyFilter all() {
            return ALL;
        }

        /**
         * Returns a filter that accepts all currencies in use as of the current date.
         *
         * @return a filter
         * @see #withDate(Date)
         * @stable ICU 4.4
         */
        public static CurrencyFilter now() {
            return ALL.withDate(new Date());
        }

        /**
         * Returns a filter that accepts all currencies ever used in the given region.
         *
         * @param region the region code
         * @return a filter
         * @see #withRegion(String)
         * @stable ICU 4.4
         */
        public static CurrencyFilter onRegion(String region) {
            return ALL.withRegion(region);
        }

        /**
         * Returns a filter that accepts the given currency.
         *
         * @param currency the currency code
         * @return a filter
         * @see #withCurrency(String)
         * @stable ICU 4.4
         */
        public static CurrencyFilter onCurrency(String currency) {
            return ALL.withCurrency(currency);
        }

        /**
         * Returns a filter that accepts all currencies in use on the given date.
         *
         * @param date the date
         * @return a filter
         * @see #withDate(Date)
         * @stable ICU 4.4
         */
        public static CurrencyFilter onDate(Date date) {
            return ALL.withDate(date);
        }

        /**
         * Returns a filter that accepts all currencies that were in use at some point between the
         * given dates, or if dates are equal, currencies in use on that date.
         *
         * @param from date on or after a currency must have been in use
         * @param to date on or before which a currency must have been in use, or if equal to from,
         *     the date on which a currency must have been in use
         * @return a filter
         * @see #withDateRange(Date, Date)
         * @stable ICU 49
         */
        public static CurrencyFilter onDateRange(Date from, Date to) {
            return ALL.withDateRange(from, to);
        }

        /**
         * Returns a filter that accepts all currencies in use on the given date.
         *
         * @param date the date as milliseconds after Jan 1, 1970
         * @stable ICU 51
         */
        public static CurrencyFilter onDate(long date) {
            return ALL.withDate(date);
        }

        /**
         * Returns a filter that accepts all currencies that were in use at some point between the
         * given dates, or if dates are equal, currencies in use on that date.
         *
         * @param from The date on or after a currency must have been in use. Measured in
         *     milliseconds since Jan 1, 1970 GMT.
         * @param to The date on or before which a currency must have been in use. Measured in
         *     milliseconds since Jan 1, 1970 GMT.
         * @stable ICU 51
         */
        public static CurrencyFilter onDateRange(long from, long to) {
            return ALL.withDateRange(from, to);
        }

        /**
         * Returns a CurrencyFilter for finding currencies that were either once used, are used, or
         * will be used as tender.
         *
         * @stable ICU 51
         */
        public static CurrencyFilter onTender() {
            return ALL.withTender();
        }

        /**
         * Returns a copy of this filter, with the specified region. Region can be null to indicate
         * no filter on region.
         *
         * @param region the region code
         * @return the filter
         * @see #onRegion(String)
         * @stable ICU 4.4
         */
        public CurrencyFilter withRegion(String region) {
            return new CurrencyFilter(region, this.currency, this.from, this.to, this.tenderOnly);
        }

        /**
         * Returns a copy of this filter, with the specified currency. Currency can be null to
         * indicate no filter on currency.
         *
         * @param currency the currency code
         * @return the filter
         * @see #onCurrency(String)
         * @stable ICU 4.4
         */
        public CurrencyFilter withCurrency(String currency) {
            return new CurrencyFilter(this.region, currency, this.from, this.to, this.tenderOnly);
        }

        /**
         * Returns a copy of this filter, with from and to set to the given date.
         *
         * @param date the date on which the currency must have been in use
         * @return the filter
         * @see #onDate(Date)
         * @stable ICU 4.4
         */
        public CurrencyFilter withDate(Date date) {
            return new CurrencyFilter(
                    this.region, this.currency, date.getTime(), date.getTime(), this.tenderOnly);
        }

        /**
         * Returns a copy of this filter, with from and to set to the given dates.
         *
         * @param from date on or after which the currency must have been in use
         * @param to date on or before which the currency must have been in use
         * @return the filter
         * @see #onDateRange(Date, Date)
         * @stable ICU 49
         */
        public CurrencyFilter withDateRange(Date from, Date to) {
            long fromLong = from == null ? Long.MIN_VALUE : from.getTime();
            long toLong = to == null ? Long.MAX_VALUE : to.getTime();
            return new CurrencyFilter(
                    this.region, this.currency, fromLong, toLong, this.tenderOnly);
        }

        /**
         * Returns a copy of this filter that accepts all currencies in use on the given date.
         *
         * @param date the date as milliseconds after Jan 1, 1970
         * @stable ICU 51
         */
        public CurrencyFilter withDate(long date) {
            return new CurrencyFilter(this.region, this.currency, date, date, this.tenderOnly);
        }

        /**
         * Returns a copy of this filter that accepts all currencies that were in use at some point
         * between the given dates, or if dates are equal, currencies in use on that date.
         *
         * @param from The date on or after a currency must have been in use. Measured in
         *     milliseconds since Jan 1, 1970 GMT.
         * @param to The date on or before which a currency must have been in use. Measured in
         *     milliseconds since Jan 1, 1970 GMT.
         * @stable ICU 51
         */
        public CurrencyFilter withDateRange(long from, long to) {
            return new CurrencyFilter(this.region, this.currency, from, to, this.tenderOnly);
        }

        /**
         * Returns a copy of this filter that filters for currencies that were either once used, are
         * used, or will be used as tender.
         *
         * @stable ICU 51
         */
        public CurrencyFilter withTender() {
            return new CurrencyFilter(this.region, this.currency, this.from, this.to, true);
        }

        /**
         * {@inheritDoc}
         *
         * @stable ICU 4.4
         */
        @Override
        public boolean equals(Object rhs) {
            return rhs instanceof CurrencyFilter && equals((CurrencyFilter) rhs);
        }

        /**
         * Type-safe override of {@link #equals(Object)}.
         *
         * @param rhs the currency filter to compare to
         * @return true if the filters are equal
         * @stable ICU 4.4
         */
        public boolean equals(CurrencyFilter rhs) {
            return Utility.sameObjects(this, rhs)
                    || (rhs != null
                            && equals(this.region, rhs.region)
                            && equals(this.currency, rhs.currency)
                            && this.from == rhs.from
                            && this.to == rhs.to
                            && this.tenderOnly == rhs.tenderOnly);
        }

        /**
         * {@inheritDoc}
         *
         * @stable ICU 4.4
         */
        @Override
        public int hashCode() {
            int hc = 0;
            if (region != null) {
                hc = region.hashCode();
            }
            if (currency != null) {
                hc = hc * 31 + currency.hashCode();
            }
            hc = hc * 31 + (int) from;
            hc = hc * 31 + (int) (from >>> 32);
            hc = hc * 31 + (int) to;
            hc = hc * 31 + (int) (to >>> 32);
            hc = hc * 31 + (tenderOnly ? 1 : 0);
            return hc;
        }

        /**
         * Returns a string representing the filter, for debugging.
         *
         * @return A string representing the filter.
         * @stable ICU 4.4
         */
        @Override
        public String toString() {
            return debugString(this);
        }

        private static boolean equals(String lhs, String rhs) {
            return (Utility.sameObjects(lhs, rhs) || (lhs != null && lhs.equals(rhs)));
        }
    }

    /**
     * Represents the raw information about fraction digits and rounding increment.
     *
     * @stable ICU 4.4
     */
    public static final class CurrencyDigits {
        /**
         * Number of fraction digits used to display this currency.
         *
         * @stable ICU 49
         */
        public final int fractionDigits;

        /**
         * Rounding increment used when displaying this currency.
         *
         * @stable ICU 49
         */
        public final int roundingIncrement;

        /**
         * Constructor for CurrencyDigits.
         *
         * @param fractionDigits the fraction digits
         * @param roundingIncrement the rounding increment
         * @stable ICU 4.4
         */
        public CurrencyDigits(int fractionDigits, int roundingIncrement) {
            this.fractionDigits = fractionDigits;
            this.roundingIncrement = roundingIncrement;
        }

        /**
         * Returns a string representing the currency digits, for debugging.
         *
         * @return A string representing the currency digits.
         * @stable ICU 4.4
         */
        @Override
        public String toString() {
            return debugString(this);
        }
    }

    /**
     * Represents a complete currency info record listing the region, currency, from and to dates,
     * and priority. Use {@link CurrencyMetaInfo#currencyInfo(CurrencyFilter)} for a list of info
     * objects matching the filter.
     *
     * @stable ICU 4.4
     */
    public static final class CurrencyInfo {
        /**
         * Region code where currency is used.
         *
         * @stable ICU 4.4
         */
        public final String region;

        /**
         * The three-letter ISO currency code.
         *
         * @stable ICU 4.4
         */
        public final String code;

        /**
         * Date on which the currency was first officially used in the region. This is midnight at
         * the start of the first day on which the currency was used, GMT. If there is no date, this
         * is Long.MIN_VALUE;
         *
         * @stable ICU 4.4
         */
        public final long from;

        /**
         * Date at which the currency stopped being officially used in the region. This is one
         * millisecond before midnight at the end of the last day on which the currency was used,
         * GMT. If there is no date, this is Long.MAX_VALUE.
         *
         * @stable ICU 4.4
         */
        public final long to;

        /**
         * Preference order of currencies being used at the same time in the region. Lower values
         * are preferred (generally, this is a transition from an older to a newer currency).
         * Priorities within a single country are unique.
         *
         * @stable ICU 49
         */
        public final int priority;

        private final boolean tender;

        /**
         * @deprecated ICU 51 Use {@link CurrencyMetaInfo#currencyInfo(CurrencyFilter)} instead.
         */
        @Deprecated
        public CurrencyInfo(String region, String code, long from, long to, int priority) {
            this(region, code, from, to, priority, true);
        }

        /**
         * Constructs a currency info.
         *
         * @internal
         * @deprecated This API is ICU internal only.
         */
        @Deprecated
        public CurrencyInfo(
                String region, String code, long from, long to, int priority, boolean tender) {
            this.region = region;
            this.code = code;
            this.from = from;
            this.to = to;
            this.priority = priority;
            this.tender = tender;
        }

        /**
         * Returns a string representation of this object, useful for debugging.
         *
         * @return A string representation of this object.
         * @stable ICU 4.4
         */
        @Override
        public String toString() {
            return debugString(this);
        }

        /**
         * Determine whether or not this currency was once used, is used, or will be used as tender
         * in this region.
         *
         * @stable ICU 51
         */
        public boolean isTender() {
            return tender;
        }
    }

    /// CLOVER:OFF
    /**
     * Returns the list of CurrencyInfos matching the provided filter. Results are ordered by
     * country code, then by highest to lowest priority (0 is highest). The returned list is
     * unmodifiable.
     *
     * @param filter the filter to control which currency info to return
     * @return the matching information
     * @stable ICU 4.4
     */
    public List<CurrencyInfo> currencyInfo(CurrencyFilter filter) {
        return Collections.emptyList();
    }

    /**
     * Returns the list of currency codes matching the provided filter. Results are ordered as in
     * {@link #currencyInfo(CurrencyFilter)}. The returned list is unmodifiable.
     *
     * @param filter the filter to control which currencies to return. If filter is null, returns
     *     all currencies for which information is available.
     * @return the matching currency codes
     * @stable ICU 4.4
     */
    public List<String> currencies(CurrencyFilter filter) {
        return Collections.emptyList();
    }

    /**
     * Returns the list of region codes matching the provided filter. Results are ordered as in
     * {@link #currencyInfo(CurrencyFilter)}. The returned list is unmodifiable.
     *
     * @param filter the filter to control which regions to return. If filter is null, returns all
     *     regions for which information is available.
     * @return the matching region codes
     * @stable ICU 4.4
     */
    public List<String> regions(CurrencyFilter filter) {
        return Collections.emptyList();
    }

    /// CLOVER:ON

    /**
     * Returns the CurrencyDigits for the currency code. This is equivalent to
     * currencyDigits(isoCode, CurrencyUsage.STANDARD);
     *
     * @param isoCode the currency code
     * @return the CurrencyDigits
     * @stable ICU 4.4
     */
    public CurrencyDigits currencyDigits(String isoCode) {
        return currencyDigits(isoCode, CurrencyUsage.STANDARD);
    }

    /**
     * Returns the CurrencyDigits for the currency code with Context Usage.
     *
     * @param isoCode the currency code
     * @param currencyUsage the currency usage
     * @return the CurrencyDigits
     * @stable ICU 54
     */
    public CurrencyDigits currencyDigits(String isoCode, CurrencyUsage currencyUsage) {
        return defaultDigits;
    }

    /**
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated protected static final CurrencyDigits defaultDigits = new CurrencyDigits(2, 0);

    static {
        CurrencyMetaInfo temp = null;
        boolean tempHasData = false;
        try {
            Class<?> clzz = Class.forName("com.ibm.icu.impl.ICUCurrencyMetaInfo");
            temp = (CurrencyMetaInfo) clzz.newInstance();
            tempHasData = true;
        } catch (Throwable t) {
            temp = new CurrencyMetaInfo();
        }
        impl = temp;
        hasData = tempHasData;
    }

    private static String dateString(long date) {
        if (date == Long.MAX_VALUE || date == Long.MIN_VALUE) {
            return null;
        }
        return Grego.timeToString(date);
    }

    private static String debugString(Object o) {
        StringBuilder sb = new StringBuilder();
        try {
            for (Field f : o.getClass().getFields()) {
                Object v = f.get(o);
                if (v != null) {
                    String s;
                    if (v instanceof Date) {
                        s = dateString(((Date) v).getTime());
                    } else if (v instanceof Long) {
                        s = dateString(((Long) v).longValue());
                    } else {
                        s = String.valueOf(v);
                    }
                    if (s == null) {
                        continue;
                    }
                    if (sb.length() > 0) {
                        sb.append(",");
                    }
                    sb.append(f.getName()).append("='").append(s).append("'");
                }
            }
        } catch (Throwable t) {
        }
        sb.insert(0, o.getClass().getSimpleName() + "(");
        sb.append(")");
        return sb.toString();
    }
}
